package sli.sli2.pcp.service;

import java.io.BufferedReader;


import java.io.PrintWriter;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Vector;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import org.bouncycastle.util.encoders.Base64;

import sli.sli2.channels.AESChannel;
import sli.sli2.channels.Channel;
import sli.sli2.channels.RSAChannel;
import sli.sli2.channels.TCPChannel;
import sli.sli2.domain.SecuredObjectKey;

/**
 * Handles the authentication with a secured object
 * @author Group 8, Khassraf Roman, Mueller Robin, Zischka Stefan
 *
 */
public class AuthenticationService {
	
	private BufferedReader in;
	private PrintWriter out;
	
	private PrivateKey myPrivateKey;
	private Vector<SecuredObjectKey> securedObjectPublicKeys;
	private LoggingService loggingService;
	
	private int securedObjectId;
	
	public AuthenticationService(BufferedReader in, PrintWriter out, PrivateKey myPrivateKey, Vector<SecuredObjectKey> securedObjectPublicKeys, LoggingService loggingService){			
		this.in = in;
		this.out = out;
		this.loggingService = loggingService;
		this.myPrivateKey = myPrivateKey;
		this.securedObjectPublicKeys = securedObjectPublicKeys;
	}
	
	/**
	 * Authenticates the Permission-Check-Provider with a Secured Object
	 * @return a secured AES encrypted Channel to communicate with the Secured Object
	 * @throws Exception - if something goes wrong during the authentication
	 */
	public Channel authenticate() throws Exception {
		
		TCPChannel tcpChannel = new TCPChannel(in, out);
		
		//Receive first Message to identify the secured object
		String encryptedFirstMessageBase64 = null;
		try {
			encryptedFirstMessageBase64 = tcpChannel.receive();

		} catch (Exception e2) {
			System.err.println("Error at receiving first message");
			throw e2;
		}
		
		//Decrypt first message
		Cipher rsaCipher = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
		rsaCipher.init(Cipher.DECRYPT_MODE, myPrivateKey);
		
		byte[] encryptedFirstMessage = decodeBase64(encryptedFirstMessageBase64);	
		
		String firstMessage = new String(rsaCipher.doFinal(encryptedFirstMessage));
		
		
		//Get Secured Object challenge and Secured Object ID from first message
		String[] split= firstMessage.split(" ");
		
		if(!split[0].equals("!login")){
			System.err.println("Error: Illegal log in command.");
			throw new Exception();
		}		
		String securedObjectChallengeBase64 = split[1];
		securedObjectId = Integer.parseInt(split[2]);
		
		PublicKey securedObjectPublicKey = getSecuredObjectPublicKey(securedObjectId);
		
		//Check if Secured Object is authorized to establish connection with Permission-Check-Provider
		if(securedObjectPublicKey == null){
			System.err.println("Authentication Error: Secured Object not in authorized to establish connection.");
			loggingService.addLogEntry(securedObjectId, 0, "Failed to authenticate secured object - public key not authorized");
			throw new Exception();
		}
		
		
		//Create RSA channel for the second authentication message
		RSAChannel rsaChannel = null;
		try {
			rsaChannel = new RSAChannel(tcpChannel, myPrivateKey, securedObjectPublicKey);
		} catch (Exception e2) {
			System.err.println("Error at creating RSA channel");
			loggingService.addLogEntry(securedObjectId, 0, "Failed to authenticate secured object");
			throw e2;
		}
		
		//Create parameters for second message;
		byte[] pcpChallengeByte = createSecureRandomNumber(32);
		String pcpChallenge = encodeBase64(pcpChallengeByte);
		
		SecretKey secretKey = null;	
		try{
			secretKey = createAESSecretKey();
		} catch(NoSuchAlgorithmException e){
			System.err.println("Algortithm for secret key creation not known");
			loggingService.addLogEntry(securedObjectId, 0, "Failed to authenticate secured object");
			throw e;
		}
				
		byte[] secretKeyByte = secretKey.getEncoded();
		byte[] ivParameter = createSecureRandomNumber(16); 
				
		//Assembly second message
		String secondMessage = "!ok "+securedObjectChallengeBase64+" "+encodeBase64(pcpChallengeByte)+" "
										+encodeBase64(secretKeyByte)+" "+encodeBase64(ivParameter);
					
		//send second message
		try {
			rsaChannel.send(secondMessage.getBytes());			
		} catch (Exception e2) {
			System.err.println("Error at sending second message");
			loggingService.addLogEntry(securedObjectId, 0, "Failed to authenticate secured object");
			throw e2;
		}
		
		//Create AES channel for further secure communication with the manager
		AESChannel aesChannel = null;				
		try {
			aesChannel = new AESChannel(tcpChannel, secretKeyByte, ivParameter);
		} catch (Exception e2) {
			System.err.println("Error at creating AESChannel.");
			loggingService.addLogEntry(securedObjectId, 0, "Failed to authenticate secured object");
			throw e2;
		}
				
		String thirdMessage = null;
		try{
			thirdMessage = aesChannel.receive();
		} catch(Exception e){
			System.err.println("Error at reveiving third message");
			loggingService.addLogEntry(securedObjectId, 0, "Failed to authenticate secured object");
			throw e;
		}
				
				
		if(!pcpChallenge.equals(thirdMessage)){
			System.err.println("Permission-Check-Provider challenge is not equal!. Authentication failed!");
			loggingService.addLogEntry(securedObjectId, 0, "Failed to authenticate secured object - challenge mismatch");
			throw new Exception();
		}
				
		loggingService.addLogEntry(securedObjectId, 0, "Secured object successfully authenticated");
		
		return aesChannel;
	}
	
	/**
	 * Returns this secured objects ID
	 * @return Secured Object ID
	 */
	public Integer getObjectId() {
		return securedObjectId;
	}
	
	/**
	 * Checks if the Secured Object is allowed to establish a connection with the Permission-Check-Provider
	 * @param securedObjectID - the id of the secured object
	 * @return the PublicKey of the Secured Object, null if Secured Object is not allowed to establish a connection
	 */
	private PublicKey getSecuredObjectPublicKey(int securedObjectID){
		
		for(SecuredObjectKey securedObject : securedObjectPublicKeys){
			
			if(securedObject.getID() == securedObjectID){
				return securedObject.getPublicKey();
				
			}		
		}
		return null;
	}
	
	/**
	 * Encodes a byte array into Base64 format
	 * @param message - the message to encode
	 * @return the encoded message
	 */
	private String encodeBase64(byte[] message){
		
		byte[] messageByte = message;
		byte[] base64MessageByte = Base64.encode(messageByte);
		
		String base64Message = new String(base64MessageByte);
		
		return base64Message;
		
	}
	
	/**
	 * Decodes a string from Base64 format
	 * @param base64Message - the message to decode
	 * @return the decoded message
	 */
	private byte[] decodeBase64(String base64Message){
		
		byte[] encryptedMessageByte = Base64.decode(base64Message.getBytes());
	
		return encryptedMessageByte;
	}
	
	/**
	 * Generates a secure random number
	 * @param byteCount - the byte count of the random number (e.g. 32 byte random number)
	 * @return the secure <byteCount> random number
	 */
	private byte[] createSecureRandomNumber(int byteCount){
		
		SecureRandom secureRandom = new SecureRandom();
		final byte[] number = new byte[byteCount];
		secureRandom.nextBytes(number);
		
		return number;
	}
	
	/**
	 * Creates a AES secret key
	 * @return the AES secret key
	 * @throws NoSuchAlgorithmException - if the algorithm is unknown for the key generator
	 */
	private SecretKey createAESSecretKey() throws NoSuchAlgorithmException{
		
		KeyGenerator generator = KeyGenerator.getInstance("AES");
		
		generator.init(256);
		
		SecretKey key = generator.generateKey();
		
		return key;
	}
	
}
